#include <QDebug>
#include <QFileInfo>
#include <QLibrary>
#include <QDirIterator>
#include "Modules.h"
#include "BinaryModule.h"
#include "CramBuilder.h"
#include "RestoreVisitor.h"

#include "grammar/python3/PythonGrammar30.h"
#include "grammar/python3/PythonGrammar30TokenManager.h"

#include "Scopes/ModuleScope.h"

Modules* Modules::_instance = NULL;

Modules::Modules():
    _binary(NULL)
{
}

Modules::~Modules()
{
    if (_binary)
        delete _binary;
    qDeleteAll(_modulesCache);
    qDebug() << "Modules count" << _modulesCache.count();
}

Modules* Modules::getModules()
{
    if (!_instance)
        _instance = new Modules();
    return _instance;
}

ModuleScope* Modules::moduleByName(const QString& moduleName, const QString& relativePath, bool useCache)
{
    if (useCache){
        if (!_modulesCache.contains(moduleName)){
            return addModuleByName(moduleName, relativePath, useCache);
        }
    } else {
        return addModuleByName(moduleName, relativePath, useCache);
    }
    return NULL;
}

ModuleScope* Modules::moduleByFileName(const QString& fileName, bool useCache)
{
    QFileInfo fi(fileName);
    if (fi.isFile() && (fi.suffix() == "py" || fi.suffix() == "pyw"))
        return parsePythonFile(fi, useCache);
    else if (fi.isFile() && (fi.suffix() == "zip" || fi.suffix() == "egg"))
        return importZipPythonPackage(fi);
    else if (fi.isDir() && fi.suffix() == "egg")
        return importEggPackage(fi);
    else if (fi.isDir() && fi.suffix() != "egg")
        return importPackage(fi);
    else if (fi.isFile() && (fi.suffix() == "so" || fi.suffix() == "pyd"))
        return importBinary(fi);
    return importBinary(fi);
}


void Modules::end()
{
    delete _instance;
}

void Modules::setImportPathes()
{
    _importPathes = binary()->importPathes();
    qDebug() << _importPathes;
}

BinaryModule* Modules::binary()
{
    if (!_binary)
        _binary = new BinaryModule();
    return _binary;
}

ModuleScope* Modules::addModuleByName(const QString& name, const QString& relativePath, bool useCache)
{
    QString fileName = fileNameByImport(name, relativePath);
    return moduleByFileName(fileName, useCache);
}

QString Modules::fileNameByImport(const QString& name, const QString& relativePath)
{
    if (name.startsWith(".")){
        // relative import
    } else {
        QString fileName = _cacheMap.findCachedByModuleName(name);
        if (!fileName.isEmpty())
            return fileName;

        if (!_importPathes.length())
            setImportPathes();

        foreach(QString path, _importPathes){
            QDirIterator iter(path, QDirIterator::NoIteratorFlags);
            while(iter.hasNext()){
                iter.next();
                QFileInfo info = iter.fileInfo();
                if (info.baseName() == name)
                    return info.absoluteFilePath();
            }
        }
    }
    return name;
}


ModuleScope* Modules::parsePythonFile(const QFileInfo& fi, bool useCache)
{
    QString parseFileName = fi.absoluteFilePath();
    bool cached = false;
    if (useCache && _cacheMap.isCached(fi)){
        cached = true;
        parseFileName = _cacheMap.getCachedFile(fi);
    }
    qDebug() << "parse" << parseFileName << "from" << fi.absoluteFilePath();
    QFile file(parseFileName);
    if (file.open(QIODevice::ReadOnly)){
        QString test = file.readAll();
        file.close();

        CharStream stream(test, test.length(), 0, 0);
        PythonGrammar30TokenManager in(&stream);
        PythonGrammar30 grammar(&in);

        ModulePtr newRoot = grammar.file_input().dynamicCast<Module>();
        ModuleScope* module = new ModuleScope(newRoot, fi.absoluteFilePath());
        _modulesCache[fi.baseName()] = module;
        _modulesCache[fi.baseName()]->buildScope();
        if (!cached && useCache){
            CramModule boot(fi.baseName());
            restoreBootStrap(newRoot, &boot);
            //qDebug() << boot.build();
            QFile out(_cacheMap.getCachedFile(fi.baseName()));
            if (out.open(QIODevice::WriteOnly)){
                out.write(boot.build().toUtf8());
            }
            _cacheMap.addToCache(fi);
        }
        return module;
    }
    return NULL;
}

ModuleScope* Modules::importZipPythonPackage(const QFileInfo& fi)
{
    return NULL;
}

ModuleScope* Modules::importEggPackage(const QFileInfo& fi)
{
    return NULL;
}

ModuleScope* Modules::importPackage(const QFileInfo& fi)
{
    return NULL;
}


ModuleScope* Modules::importBinary(const QFileInfo& fi)
{
    if (_cacheMap.isCached(fi)){
        return parsePythonFile(fi, true);
    }
    QString moduleName = fi.baseName();
    CramModule boot(moduleName);

    PyObject* mod = binary()->importModule(moduleName);
    processScope(mod, &boot);
    QFile out(_cacheMap.getCachedFile(fi));
    if (out.open(QIODevice::WriteOnly)){
        out.write(boot.build().toUtf8());
        out.close();
    }
    binary()->free(mod);
    _cacheMap.addToCache(fi);
    return parsePythonFile(fi);
}

void Modules::processScope(PyObject* obj, CramBuilder* builder, bool onlyPublic)
{
    QMap<QString, PyObject*> info = binary()->dir(obj);
    foreach(QString key, info.keys()){
        if (!info[key])
            continue;

        QString type = binary()->typeName(info[key]);
        //qDebug() << key << type;
        if (type == "str"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            QString doc = binary()->attrString(obj, key);
            if (key == "__doc__" || doc.split("\n").length() > 1)
                *builder << builder->ident << key << "=\"\"\"" << doc << "\"\"\"\n";
            else
                *builder << builder->ident << key << "=\"" << doc << "\"\n";
            //builder->addGlobalStr(key, binary()->attrString(obj, key));
        } else if (type == "module"){
            if (key != "__builtins__" && key != "builtins")
                *builder << builder->ident << "import " << key;
            //builder->addImport(key);
        } else if (type == "type"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            CramClass *cls = builder->addClass(key, binary()->getMro(info[key]), binary()->getDoc(info[key]));
            //cls->setBases(binary()->getMro(info[key]));
            //cls->__doc__ = binary()->getDoc(info[key]);
            processScope(info[key], cls, true);
        } else if (type == "function" || type == "method_descriptor" || type == "builtin_function_or_method"){
            builder->addFunction(key, binary()->funcArgs(info[key], key), binary()->getDoc(info[key]), binary()->funcReturn(info[key], key));
            //fnc->__doc__ = ;
            //fnc->setArgs(binary()->funcArgs(info[key], key));
        }
        //qDebug() << key;
    }
    binary()->free(info);
}

void Modules::restoreBootStrap(const SimpleNodePtr& root, CramBuilder* builder)
{
    RestoreVisitor visit(builder);
    visit.traverse(root);
}
